/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.codetail.animation;

import android.support.annotation.FloatRange;

/**
 * Spring Force defines the characteristics of the spring being used in the animation.
 * <p>
 * By configuring the stiffness and damping ratio, callers can create a spring with the look and
 * feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
 * is, the harder it is to stretch it, the faster it undergoes dampening.
 * <p>
 * Spring damping ratio describes how oscillations in a system decay after a disturbance.
 * When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
 * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
 * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
 * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any
 * damping (i.e. damping ratio = 0), the mass will oscillate forever.
 */
final class SpringForce implements Force {
  /**
   * Stiffness constant for extremely stiff spring.
   */
  public static final float STIFFNESS_HIGH = 10_000f;
  /**
   * Stiffness constant for medium stiff spring. This is the default stiffness for spring force.
   */
  public static final float STIFFNESS_MEDIUM = 1500f;
  /**
   * Stiffness constant for a spring with low stiffness.
   */
  public static final float STIFFNESS_LOW = 200f;
  /**
   * Stiffness constant for a spring with very low stiffness.
   */
  public static final float STIFFNESS_VERY_LOW = 50f;

  /**
   * Damping ratio for a very bouncy spring. Note for under-damped springs
   * (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
   */
  public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f;
  /**
   * Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring
   * force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio,
   * the more bouncy the spring.
   */
  public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f;
  /**
   * Damping ratio for a spring with low bounciness. Note for under-damped springs
   * (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
   */
  public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f;
  /**
   * Damping ratio for a spring with no bounciness. This damping ratio will create a critically
   * damped spring that returns to equilibrium within the shortest amount of time without
   * oscillating.
   */
  public static final float DAMPING_RATIO_NO_BOUNCY = 1f;

  // This multiplier is used to calculate the velocity threshold given a certain value threshold.
  // The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
  // is a reasonable threshold.
  private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0;

  // Default threshold for different properties.
  static final double VALUE_THRESHOLD_IN_PIXEL = 0.75;
  static final double VALUE_THRESHOLD_ALPHA = VALUE_THRESHOLD_IN_PIXEL / 255.0;
  static final double VALUE_THRESHOLD_SCALE = VALUE_THRESHOLD_IN_PIXEL / 500.0;
  static final double VALUE_THRESHOLD_ROTATION = VALUE_THRESHOLD_IN_PIXEL / 360.0;

  // Natural frequency
  double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM);
  // Damping ratio.
  double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY;

  // Value to indicate an unset state.
  private static final double UNSET = Double.MAX_VALUE;

  // Indicates whether the spring has been initialized
  private boolean mInitialized = false;

  // Threshold for velocity and value to determine when it's reasonable to assume that the spring
  // is approximately at rest.
  private double mValueThreshold = VALUE_THRESHOLD_IN_PIXEL;
  private double mVelocityThreshold = VALUE_THRESHOLD_IN_PIXEL * VELOCITY_THRESHOLD_MULTIPLIER;

  // Intermediate values to simplify the spring function calculation per frame.
  private double mGammaPlus;
  private double mGammaMinus;
  private double mDampedFreq;

  // Final position of the spring. This must be set before the start of the animation.
  private double mFinalPosition = UNSET;

  // Internal state to hold a value/velocity pair.
  private final MassState mMassState = new MassState();

  // Internal state for value/velocity pair.
  static class MassState {
    float mValue;
    float mVelocity;
  }

  /**
   * Creates a spring force. Note that final position of the spring must be set through
   * {@link #setFinalPosition(float)} before the spring animation starts.
   */
  public SpringForce() {
    // No op.
  }

  /**
   * Creates a spring with a given final rest position.
   *
   * @param finalPosition final position of the spring when it reaches equilibrium
   */
  public SpringForce(float finalPosition) {
    mFinalPosition = finalPosition;
  }

  /**
   * Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to
   * the object attached when the spring is not at the final position. Default stiffness is
   * {@link #STIFFNESS_MEDIUM}.
   *
   * @param stiffness non-negative stiffness constant of a spring
   * @return the spring force that the given stiffness is set on
   * @throws IllegalArgumentException if the given spring stiffness is negative.
   */
  public SpringForce setStiffness(@FloatRange(from = 0.0) float stiffness) {
    if (stiffness < 0) {
      throw new IllegalArgumentException("Spring stiffness constant cannot be negative");
    }
    mNaturalFreq = Math.sqrt(stiffness);
    // All the intermediate values need to be recalculated.
    mInitialized = false;
    return this;
  }

  /**
   * Gets the stiffness of the spring.
   *
   * @return the stiffness of the spring
   */
  public float getStiffness() {
    return (float) (mNaturalFreq * mNaturalFreq);
  }

  /**
   * Spring damping ratio describes how oscillations in a system decay after a disturbance.
   * <p>
   * When damping ratio > 1 (over-damped), the object will quickly return to the rest position
   * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
   * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
   * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without
   * any damping (i.e. damping ratio = 0), the mass will oscillate forever.
   * <p>
   * Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}.
   *
   * @param dampingRatio damping ratio of the spring, it should be non-negative
   * @return the spring force that the given damping ratio is set on
   * @throws IllegalArgumentException if the {@param dampingRatio} is negative.
   */
  public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) {
    if (dampingRatio < 0) {
      throw new IllegalArgumentException("Damping ratio must be non-negative");
    }
    mDampingRatio = dampingRatio;
    // All the intermediate values need to be recalculated.
    mInitialized = false;
    return this;
  }

  /**
   * Returns the damping ratio of the spring.
   *
   * @return damping ratio of the spring
   */
  public float getDampingRatio() {
    return (float) mDampingRatio;
  }

  /**
   * Sets the rest position of the spring.
   *
   * @param finalPosition rest position of the spring
   * @return the spring force that the given final position is set on
   */
  public SpringForce setFinalPosition(float finalPosition) {
    mFinalPosition = finalPosition;
    return this;
  }

  /**
   * Returns the rest position of the spring.
   *
   * @return rest position of the spring
   */
  public float getFinalPosition() {
    return (float) mFinalPosition;
  }

  /*********************** Below are private APIs *********************/

  /**
   * @hide
   */
  @Override
  public float getAcceleration(float lastDisplacement, float lastVelocity) {

    lastDisplacement -= getFinalPosition();

    double k = mNaturalFreq * mNaturalFreq;
    double c = 2 * mNaturalFreq * mDampingRatio;

    return (float) (-k * lastDisplacement - c * lastVelocity);
  }

  /**
   * @hide
   */
  @Override
  public boolean isAtEquilibrium(float value, float velocity) {
    return Math.abs(velocity) < mVelocityThreshold
        && Math.abs(value - getFinalPosition()) < mValueThreshold;
  }

  /**
   * Initialize the string by doing the necessary pre-calculation as well as some sanity check
   * on the setup.
   *
   * @throws IllegalStateException if the final position is not yet set by the time the spring
   * animation has started
   */
  private void init() {
    if (mInitialized) {
      return;
    }

    if (mFinalPosition == UNSET) {
      throw new IllegalStateException("Error: Final position of the spring must be"
          + " set before the animation starts");
    }

    if (mDampingRatio > 1) {
      // Over damping
      mGammaPlus = -mDampingRatio * mNaturalFreq
          + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
      mGammaMinus = -mDampingRatio * mNaturalFreq
          - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
    } else if (mDampingRatio >= 0 && mDampingRatio < 1) {
      // Under damping
      mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
    }

    mInitialized = true;
  }

  /**
   * Internal only call for Spring to calculate the spring position/velocity using
   * an analytical approach.
   */
  MassState updateValues(double lastDisplacement, double lastVelocity, long timeElapsed) {
    init();

    double deltaT = timeElapsed / 1000d; // unit: seconds
    lastDisplacement -= mFinalPosition;
    double displacement;
    double currentVelocity;
    if (mDampingRatio > 1) {
      // Overdamped
      double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity)
          / (mGammaMinus - mGammaPlus);
      double coeffB = (mGammaMinus * lastDisplacement - lastVelocity)
          / (mGammaMinus - mGammaPlus);
      displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT)
          + coeffB * Math.pow(Math.E, mGammaPlus * deltaT);
      currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT)
          + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT);
    } else if (mDampingRatio == 1) {
      // Critically damped
      double coeffA = lastDisplacement;
      double coeffB = lastVelocity + mNaturalFreq * lastDisplacement;
      displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT);
      currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT)
          * (-mNaturalFreq) + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT);
    } else {
      // Underdamped
      double cosCoeff = lastDisplacement;
      double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq
          * lastDisplacement + lastVelocity);
      displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
          * (cosCoeff * Math.cos(mDampedFreq * deltaT)
          + sinCoeff * Math.sin(mDampedFreq * deltaT));
      currentVelocity = displacement * (-mNaturalFreq) * mDampingRatio
          + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
          * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT)
          + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT));
    }

    mMassState.mValue = (float) (displacement + mFinalPosition);
    mMassState.mVelocity = (float) currentVelocity;
    return mMassState;
  }

  /**
   * This threshold defines how close the animation value needs to be before the animation can
   * finish. This default value is based on the property being animated, e.g. animations on alpha,
   * scale, translation or rotation would have different thresholds. This value should be small
   * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
   * animations take seconds to finish.
   *
   * @param threshold the difference between the animation value and final spring position that is
   * allowed to end the animation when velocity is very low
   */
  void setDefaultThreshold(double threshold) {
    mValueThreshold = Math.abs(threshold);
    mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
  }
}
